package job

import (
	"context"
	"go.uber.org/zap"
	"golang.org/x/sync/semaphore"
	"jk-time/webook/internal/domain"
	"jk-time/webook/internal/service"
	"time"
)

// Scheduler 调度器 抢占-运行-释放
type Scheduler struct {
	execs     map[string]Executor
	svc       service.JobService
	dbTimeout time.Duration
	interval  time.Duration
	limiter   *semaphore.Weighted
}

func NewScheduler(svc service.JobService) *Scheduler {
	return &Scheduler{
		execs:     make(map[string]Executor, 8),
		interval:  time.Second,
		svc:       svc,
		dbTimeout: time.Second,
		// 假如说最多只有 100 个在运行
		limiter: semaphore.NewWeighted(100),
	}
}
func (s *Scheduler) RegisterJob(ctx context.Context, j CronJob) error {
	return s.svc.AddJob(ctx, j)
}
func (s *Scheduler) RegisterExecutor(exec Executor) {
	name := exec.Name()
	s.execs[name] = exec
}

// Start 开始调度。当被取消，或者超时的时候，就会结束调度
func (s *Scheduler) Start(ctx context.Context) error {
	for {
		if ctx.Err() != nil {
			// 已经超时了，或者被取消运行，大多数时候，都是被取消了，或者说关闭了
			return ctx.Err()
		}
		err := s.limiter.Acquire(ctx, 1)
		if err != nil {
			// 正常来说，只有 ctx 超时或者取消才会进来这里
			return err
		}

		dbCtx, cancel := context.WithTimeout(ctx, s.dbTimeout)
		j, err := s.svc.Preempt(dbCtx)
		cancel()
		if err != nil {
			// 没有抢占到，进入下一个循环
			// 这里可以考虑睡眠一段时间
			// 你也可以进一步细分不同的错误，如果是可以容忍的错误，
			// 就继续，不然就直接 return
			zap.L().Error("抢占任务失败", zap.Error(err))
			continue
		}
		exec, ok := s.execs[j.Executor] //local
		if !ok {
			// 不支持的执行方式。
			// 比如说，这里要求的runner是调用 gRPC，我们就不支持
			zap.L().Error("未找到对应的执行器", zap.String("executor", j.Executor))
			continue
		}
		// 要单独开一个 goroutine 来执行，这样我们就可以进入下一个循环了
		go func() {
			defer func() {
				s.limiter.Release(1)
				//测试屏蔽
				err = j.CancelFunc()
				if err != nil {
					zap.L().Error("释放任务失败", zap.Int64("id", j.Id), zap.Error(err))
				}
			}()

			err := exec.Exec(ctx, j)
			if err != nil {
				zap.L().Error("任务执行失败", zap.Int64("id", j.Id), zap.Error(err))
				return
			}
			err = s.svc.ResetNextTime(ctx, j)
			if err != nil {
				zap.L().Error("更新下一次的执行失败", zap.Int64("id", j.Id), zap.Error(err))
			}
		}()
	}
}

type CronJob = domain.Job
